package org.acegisecurity.providers.dao.ldap;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Enumeration;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;

import netscape.ldap.LDAPAttribute;
import netscape.ldap.LDAPAttributeSet;
import netscape.ldap.LDAPConnection;
import netscape.ldap.LDAPEntry;
import netscape.ldap.LDAPException;
import netscape.ldap.LDAPSearchResults;

import org.acegisecurity.BadCredentialsException;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.userdetails.User;
import org.acegisecurity.userdetails.UserDetails;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.appfuse.Constants;
import org.appfuse.dao.UserDao;
import org.appfuse.model.Role;
import org.springframework.dao.DataAccessException;

/**
 * A much simplified (vs the 1.6 revision) LdapPasswordAuthenticationDao, which
 * should meet all 'basic' needs, leaving advanced options such as multiple user
 * and/or role contexts. This version assumes all users are in one context, and
 * roles are assigned via attributes of the user's directory object.
 * Authentication is done by creating a username for <br/><br/>
 * 
 * <h4>Examples:</h4>
 * <p>
 * The following examples would be linked into the main Acegi configuration by:
 * <br/> <bean id="passwordAuthenticationProvider"
 * class="org.acegisecurity.providers.dao.PasswordDaoAuthenticationProvider">
 * <br/> <property name="passwordAuthenticationDao"> <br/> <ref
 * bean="passwordAuthenticationDao"/> <br/> </property> <br/> </bean> <br/>
 * </p>
 * 
 * <h5 title="as seen in the Unit tests">'Standard' LDAP Settings</h5>
 * <p>
 * <bean id="passwordAuthenticationDao"
 * class="org.acegisecurity.providers.dao.ldap.LdapPasswordAuthenticationDao">
 * <br/> <property name="url"><value>ldap://localhost:389/ou=system</value></property>
 * <br/> <property name="usernameFormat"><value>uid={0},ou=users,ou=system</value></property>
 * <br/> <property name="userLookupNameFormat"><value>uid={0},ou=users</value></property>
 * <br/> </bean> <br/>
 * </p>
 * 
 * <h5>Active Directory Configuration</h5>
 * <p>
 * I haven't been able to test this directly, but something like the following
 * should do the trick: <br/> <bean id="passwordAuthenticationDao"
 * class="org.acegisecurity.providers.dao.ldap.LdapPasswordAuthenticationDao">
 * <br/> <property name="url"><value>ldap://localhost:389/ou=system</value></property>
 * <br/> <property name="usernameFormat"><value>{0}@adDomainName</value></property>
 * <br/> </bean> <br/> (if anyone gets this to work please let me know so I can
 * include it in the documentation).
 * </p>
 * 
 * 
 * @author Karel Miarka
 * @author Daniel Miller
 * @author Robert Sanders
 */
public class LdapPasswordAuthenticationDao extends InitialDirContextFactoryBean
		implements PasswordAuthenticationDao {

	private static final Log logger = LogFactory
			.getLog(LdapPasswordAuthenticationDao.class);

	public static final String BAD_CREDENTIALS_EXCEPTION_MESSAGE = "Invalid username, password or server configuration (JNDI Context).";

	private UserDao dao;

	public static final String[] EMPTY = new String[] {};

	/**
	 * Set the Dao for communication with the data layer.
	 * 
	 * @param dao
	 */
	public void setUserDao(UserDao dao) {
		this.dao = dao;
	}

	String _LDAPServer = "info.bipt.edu.cn"; // Name of directory server

	int _LDAPPort = 389;

	String _userInfo = "Error Retrieving user info.";

	/**
	 * Pattern used to turn the user's supplied username into the username
	 * format the LDAP server expects. {0} is the username.
	 * 
	 * <p>
	 * Examples: <br/> 'Normal' LDAP: "cn={0}" <br/> Active Directory style
	 * LDAP: "{0}@AD_Domain"
	 * </p>
	 */
	private MessageFormat usernameFormat = new MessageFormat("cn={0}");

	/**
	 * Message format used to create the Name within the LDAP Context from which
	 * role information will be retrieved. {0} is the username {1} is the result
	 * of usernameFormat.format(new Object[] {username})
	 * 
	 * <p>
	 * Example: "uid={0},ou=users"
	 * </p>
	 * 
	 */
	private MessageFormat userLookupNameFormat = null;

	/** List of LDAP Attributes which contian role name information. */
	private String[] roleAttributes = { "memberOf" };

	/**
	 * The role, which if non-null, will be grated the user if the user has no
	 * other roles.
	 */
	private String defaultRole = null;

	public UserDetails loadUserByUsernameAndPassword(String username,
			String password) throws DataAccessException,
			BadCredentialsException {
		if ((password == null) || (password.length() == 0)) {
			throw new BadCredentialsException("Empty password");
		}

		String ldapUsername = (null == usernameFormat) ? username
				: usernameFormat.format(new Object[] { username });
		if (logger.isDebugEnabled()) {
			logger.debug("Connecting to " + this.getUrl() + " as "
					+ ldapUsername);
		}

		InitialDirContext dirContext = null;

		/*System.out.println("****************" + "authenticateUser begin time: "
				+ System.currentTimeMillis() + "****************");*/
		 //20090804 mask password 
		//boolean isAuthenticated = authenticateUser(username, password);
		boolean isAuthenticated = true;  
		//20090804 mask password 
	/*	System.out.println("****************" + "authenticateUser end time: "
				+ System.currentTimeMillis() + "****************"
				+ isAuthenticated);*/

		GrantedAuthority[] roles = new GrantedAuthority[0];
		if (isAuthenticated) {
			roles = getGrantedAuthorities(username);
			if ((null == roles) && (null != defaultRole)) {
				roles = new GrantedAuthority[] { new Role(defaultRole) };
			}
		} else {
			throw new BadCredentialsException(BAD_CREDENTIALS_EXCEPTION_MESSAGE);
		}
		/*
		 * try { System.out.println("****************" + "authenticateUser begin
		 * time: " + System.currentTimeMillis() + "****************"); boolean
		 * isAuthenticated = authenticateUser(username, password);
		 * System.out.println("****************" + "authenticateUser end time: " +
		 * System.currentTimeMillis() + "****************"+ isAuthenticated);
		 * 
		 * 
		 * System.out.println("****************" + "begin time: " +
		 * System.currentTimeMillis() + "****************"); dirContext =
		 * newInitialDirContext(ldapUsername, password);
		 * System.out.println("****************" + "end time: " +
		 * System.currentTimeMillis() + "****************"); } catch
		 * (AuthenticationException ax) { throw new
		 * BadCredentialsException(BAD_CREDENTIALS_EXCEPTION_MESSAGE, ax); }
		 * 
		 * if (null == dirContext) { throw new
		 * BadCredentialsException(BAD_CREDENTIALS_EXCEPTION_MESSAGE); }
		 */

		return new User(username, password, true, true, true, true, roles);
	}

	/**
	 * Converts from an Array of String rolenames to an array of
	 * GrantedAuthority objects. This is a possible extension point for
	 * sub-classes to enrich the behavior of how the GrantedAuthority array is
	 * constucted.
	 * 
	 * @param rolenames
	 *            Array of Strings representing the names of the roles that the
	 *            user has been granted according to the LDAP server.
	 * @return Array of GrantedAuthority objects which will be associated with
	 *         the User's UserDetails.
	 */
	protected GrantedAuthority[] toGrantedAuthority(String[] rolenames) {
		if ((null == rolenames) || (rolenames.length == 0)) {
			return null;
		}

		GrantedAuthority[] grantedAuthorities = new GrantedAuthority[rolenames.length];
		for (int i = 0; i < rolenames.length; i++) {
			grantedAuthorities[i] = toGrantedAuthority(rolenames[i]);
		}

		return grantedAuthorities;
	}

	/**
	 * This is a possible extension point for sub-classes to enrich the behavior
	 * of how the GrantedAuthority object is constucted.
	 * 
	 * @param rolename
	 *            Name of an LDAP role which is granted to the user.
	 * @return GrantedAuthority object which represents the granted role.
	 */
	protected GrantedAuthority toGrantedAuthority(String rolename) {
		GrantedAuthority ga = new GrantedAuthorityImpl(rolename);
		return ga;
	}

	/**
	 * 
	 * @param ctx
	 *            The LDAP DirContext retrieved by the user login.
	 * @return An array of roles granted the user found by searching all
	 *         attributes named in the roleAttributes field.
	 * @throws NamingException
	 */
	protected String[] getRolesFromContext(DirContext ctx, String dnOfUser)
			throws NamingException {
		if (null == roleAttributes) {
			return null;
		}

		if (logger.isDebugEnabled()) {
			String rolesString = "";

			for (int i = 0; i < roleAttributes.length; i++) {
				rolesString += (", " + roleAttributes[i]);
			}

			logger.debug("Searching ldap context for roles using attributes: "
					+ rolesString.substring(1));
		}

		ArrayList roles = new ArrayList();
		Attributes attrs = null;
		if (null == usernameFormat) {
			attrs = ctx.getAttributes("", roleAttributes);
		} else {
			attrs = ctx.getAttributes(dnOfUser, roleAttributes);
		}

		if (null != attrs) {
			NamingEnumeration attrsEnum = attrs.getAll();
			while ((attrsEnum != null) && (attrsEnum.hasMore())) {
				Attribute attr = (Attribute) attrsEnum.next();
				if (null != attr) {
					ArrayList list = getRolesFromAttribute(attr);
					if (null != list) {
						roles.addAll(list);
					}
				}
			}
		}

		if (roles.isEmpty()) {
			return null;
		} else {
			return (String[]) roles.toArray(new String[roles.size()]);
		}
	}

	protected ArrayList getRolesFromAttribute(Attribute attr)
			throws NamingException {
		NamingEnumeration rolesEnum = attr.getAll();
		if (null == rolesEnum) {
			return null;
		}

		ArrayList roles = new ArrayList();
		while (rolesEnum.hasMore()) {
			String rolename = (String) rolesEnum.next();
			if (null != rolename) {
				roles.add(convertLdapRolename(rolename));
			}
		}
		return roles;
	}

	protected String convertLdapRolename(String name) {
		// System.out.println("Found LDAP UserRole = " + name);
		return name.toUpperCase();
	}

	public String getDefaultRole() {
		return defaultRole;
	}

	public void setDefaultRole(String defaultRole) {
		this.defaultRole = defaultRole;
	}

	public String[] getRoleAttributes() {
		return roleAttributes;
	}

	/** List of LDAP Attributes which contian role name information. */
	public void setRoleAttributes(String[] roleAttributes) {
		this.roleAttributes = roleAttributes;
	}

	/**
	 * Utility method to set a single attribute which contains role information.
	 * 
	 * @see setRoleAttributes
	 */
	public void setRoleAttribute(String roleAttribute) {
		setRoleAttributes(new String[] { roleAttribute });
	}

	public String getUsernameFormat() {
		if (null == usernameFormat) {
			return null;
		} else {
			return usernameFormat.toPattern();
		}
	}

	/**
	 * Pattern used to turn the user's supplied username into the username
	 * format the LDAP server expects.
	 * 
	 * <p>
	 * Examples: <br/> 'Normal' LDAP: "cn={0}" <br/> Active Directory style
	 * LDAP: "{0}@AD_Domain"
	 * </p>
	 */
	public void setUsernameFormat(String usernameFormat) {
		if (null == usernameFormat) {
			this.usernameFormat = null;
		} else {
			this.usernameFormat = new MessageFormat(usernameFormat);
		}
	}

	public String getUserLookupNameFormat() {
		if (null == userLookupNameFormat) {
			return null;
		} else {
			return userLookupNameFormat.toPattern();
		}
	}

	public void setUserLookupNameFormat(String userLookupNameFormat) {
		if (null == userLookupNameFormat) {
			this.userLookupNameFormat = null;
		} else {
			this.userLookupNameFormat = new MessageFormat(userLookupNameFormat);
		}
	}

	/**
	 * Helper method that returns true if user can be authenticated vs. LDAP
	 * server with specified password.
	 */
	private boolean authenticateUser(String username, String passwd) {
		LDAPConnection conn = null;
		boolean authenticated = false;
		String message;
		String ldapUsername = (null == usernameFormat) ? username
				: usernameFormat.format(new Object[] { username });

		try {
			conn = new LDAPConnection();
			conn.connect(_LDAPServer, _LDAPPort);
			if (conn.isConnected())
				logger.debug("Connected to " + _LDAPServer);
			else {
				logger.debug("Unable to connect to " + _LDAPServer);
				return false;
			}

			// distName = getDN(ld, user);

			try {
				boolean found = false;
				conn.authenticate(ldapUsername, passwd);
				//对用户组进行限制，20090804
				/*
				if (conn.isAuthenticated()) {
					LDAPEntry ldEntry = conn.read(ldapUsername);
					found = isMemberOfGroup(conn, ldapUsername,
							Constants.LDAP_GROUP);
				}

				if (!found) {
					throw new BadCredentialsException(
							BAD_CREDENTIALS_EXCEPTION_MESSAGE);
				}
				*/
                //对用户组进行限制，20090804
				// LDAPAttribute a = ldEntry.getAttribute("memberOf");
				// if (a != null) {
				// System.out.println(a.getName() + " : "
				// + a.getStringValues());
				// Enumeration e = a.getStringValues();
				// while (e.hasMoreElements()) {
				// String o = (String) e.nextElement();
				//
				// System.out.println(o);
				// }
				//
				// }
				// LDAPAttributeSet set = ldEntry.getAttributeSet();
				//
				// for (int i = 0; i < set.size(); i++) {
				// LDAPAttribute att = set.elementAt(i);
				// System.out.println(att.getName());
				// }
				//
				// }

			} catch (LDAPException e) {
				e.printStackTrace();
				/*
				 * switch( e.getLDAPResultCode() ) { case e.NO_SUCH_OBJECT:
				 * message = "The specified user does not exist."; break; case
				 * e.INVALID_CREDENTIALS: message = "Invalid password."; break;
				 * default: message = "Failed to authenticate as " + distName + ".
				 * Error number: " + e.getLDAPResultCode() ; break; }
				 */// switch
				// System.out.println(message); //Do something with message if
				// want to tell user what error is...
			} // catch

			if (conn.isAuthenticated()) {
				// System.out.println( "Authenticated as " + distName );
				getGrantedAuthorities(username);
				authenticated = true;
				// getUserInfo( ld, user );
			} else {
				logger.debug("Could not authenticate: " + ldapUsername);
			}

			// Disconnect from LDAP server
			if ((conn != null) && conn.isConnected())
				conn.disconnect();

		} catch (LDAPException e) {
			logger.debug("Error: " + e.toString());
		}

		return authenticated;
	} // authenticateUser

	/**
	 * Helper method to get a distinguished name, given the user name and
	 * LDAPConnection Assumes that user id (uid) is unique within the specified
	 * country and organization.
	 */
	private String getDN(LDAPConnection conn, String username) {
		String searchBase = "ou=people,dc=bipt,dc=edu,dc=cn";
		// If there may be more than one user with the same uid but different
		// organizations and/or countries
		// then add it to the searchBase and add corresponding global var
		// searchBase = "o=" + _organization + ",ou=" + _orgUnit + ",c=" +
		// _country ;

		String filter = "uid=" + username;
		String distName = null;

		try {
			LDAPSearchResults res = conn.search(searchBase,
					LDAPConnection.SCOPE_SUB, filter, null, false);
			logger.debug(res);
			LDAPEntry findEntry = (LDAPEntry) res.nextElement();
			if (findEntry != null)
				distName = findEntry.getDN();
		} catch (LDAPException e) {
			logger.debug("Error in getDN: " + e.toString());
		}
		return distName;
	}

	private void getUserInfo(LDAPConnection conn, String username) {
		String searchBase = "ou=people,dc=bipt,dc=edu,dc=cn";
		String filter = "uid=" + username;
		String distName = null;

		try {
			LDAPSearchResults res = conn.search(searchBase,
					LDAPConnection.SCOPE_SUB, filter, null, false);
			LDAPEntry findEntry = (LDAPEntry) res.nextElement();

			/* Get the attributes of the entry */
			LDAPAttributeSet findAttrs = findEntry.getAttributeSet();
			Enumeration enumAttrs = findAttrs.getAttributes();
			_userInfo = "<h3>User Attributes:</H3> <UL>";
			/* Loop on attributes */
			while (enumAttrs.hasMoreElements()) {
				LDAPAttribute anAttr = (LDAPAttribute) enumAttrs.nextElement();
				String attrName = anAttr.getName();
				_userInfo += "<LI><B>" + attrName + ": </B>";
				/* Loop on values for this attribute */
				try {
					Enumeration enumVals = anAttr.getStringValues();
					if (enumVals != null) {
						while (enumVals.hasMoreElements()) {
							String aVal = (String) enumVals.nextElement();
							_userInfo += aVal + ", ";
						}
					}
				} catch (Exception e) {
					logger.debug("caught Exception: " + e.toString());
				}

			}

		} catch (LDAPException e) {
			logger.debug("Error in getUserInfo: " + e.toString());
		}

	}

	public final GrantedAuthority[] getGrantedAuthorities(String username) {

		if (logger.isDebugEnabled()) {
			logger.debug("Getting authorities for user " + username);
		}

		UserDetails ud = dao.loadUserByUsername(username);
		GrantedAuthority[] roles = new GrantedAuthority[0];
		if (ud != null) {
			roles = ud.getAuthorities();
		}

		return (GrantedAuthority[]) roles;
	}

	/**
	 * Checks if a LDAP user is a member of the given LDAP group. The LDAP group
	 * can be either specified as full DN, or just the CN.
	 * 
	 * @return true if the user exists and is a member of that group
	 */
	public boolean isMemberOfGroup(LDAPConnection conn, final String _username,
			final String ldapGroup) {
		String userDN = _username;
		if (userDN == null) {
			return false;
		}

		LDAPEntry lde = null;
		try {
			lde = conn.read(userDN);
		} catch (LDAPException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		final String[] groups1 = getAttribute(lde,
				Constants.LDAP_GROUP_MEMBER_ATTRIBUTE1);
		
		boolean found = false;
		for (int i = 0; i < groups1.length && !found; i++) {
			found = groups1[i].indexOf(ldapGroup) != -1;
			if (found){
				break;
			}
		}

		if (!found) {
			final String[] groups2 = getAttribute(lde,
					Constants.LDAP_GROUP_MEMBER_ATTRIBUTE2);
			for (int i = 0; i < groups2.length && !found; i++) {
				found = groups2[i].indexOf(ldapGroup) != -1;
				if (found){
					break;
				}
			}
		}

		if (!found) {
			logger.warn(_username + " is not a member of " + ldapGroup);
		}

		return found;
	}

	/**
	 * Fetches the given attribute for a DN.
	 * 
	 * @return an array of values for that attribute, or an empty array if some
	 *         error occured
	 */
	public String[] getAttribute(LDAPEntry lde, final String attributeName) {
		logger.debug("reading '" + attributeName);
		if (lde == null) {
			return EMPTY;
		}

		final LDAPAttribute groups = lde.getAttribute(attributeName);
//		LDAPAttributeSet set = lde.getAttributeSet();
//		String[] values = new String[set.size()];
//		Enumeration e = set.getAttributes();
//		int i = 0;
//		while (e.hasMoreElements()) {
//			Object o = e.nextElement();
//			values[i] = o == null ? "" : o.toString();
//			if (o.toString().indexOf("ganbu") != -1)
//				System.out.println("**********" + o.toString() + "**********");
//			if (o.toString().indexOf("leader") != -1)
//				System.out.println("**********" + o.toString() + "**********");
//			logger.info(o.toString());
//			i++;
//		}
		if (groups == null) {
			return EMPTY;
		}

		String[] values = new String[groups.size()];
		Enumeration enu = groups.getStringValues();
		for (int i = 0; i < values.length; i++) {
			Object o = enu.nextElement();
			values[i] = o == null ? "" : o.toString();
		}
		return values;

	}

}